package cn.lingyangwl.agile.infra.file.service.impl;

import cn.lingyangwl.agile.common.core.exception.enums.GlobalErrorEnum;
import cn.lingyangwl.agile.common.security.utils.SecurityUtils;
import cn.lingyangwl.agile.infra.file.assembly.FileRecordAssembly;
import cn.lingyangwl.agile.infra.file.channel.IStorageChannel;
import cn.lingyangwl.agile.infra.file.channel.StorageChannelManager;
import cn.lingyangwl.agile.infra.file.manager.FileConfigManager;
import cn.lingyangwl.agile.infra.file.mapper.FileRecordMapper;
import cn.lingyangwl.agile.infra.file.modle.bo.FilePathBO;
import cn.lingyangwl.agile.infra.file.modle.rqrs.FileRecordSaveResp;
import cn.lingyangwl.agile.infra.file.modle.rqrs.UploadFileReq;
import cn.lingyangwl.agile.infra.file.service.FileRecordService;
import cn.lingyangwl.agile.infra.file.utils.FileUtils;
import cn.lingyangwl.agile.infra.module.dict.DictItemApi;
import cn.lingyangwl.agile.infra.module.file.FileProperties;
import cn.lingyangwl.agile.infra.module.file.manager.FileManager;
import cn.lingyangwl.agile.infra.module.file.model.entity.FileConfig;
import cn.lingyangwl.agile.infra.module.file.model.entity.FileRecord;
import cn.lingyangwl.agile.infra.module.file.model.rqrs.FileRecordPageReq;
import cn.lingyangwl.agile.infra.module.file.model.rqrs.FileRecordResp;
import cn.lingyangwl.agile.model.enums.BoolEnum;
import cn.lingyangwl.agile.model.module.auth.LoginUser;
import cn.lingyangwl.framework.core.utils.FileDownloadUtils;
import cn.lingyangwl.framework.data.mate.annotations.FieldBindHandle;
import cn.lingyangwl.framework.mybatis.plus.utils.PageUtil;
import cn.lingyangwl.framework.tool.core.CollectionUtils;
import cn.lingyangwl.framework.tool.core.StringUtils;
import cn.lingyangwl.framework.tool.core.exception.Assert;
import cn.lingyangwl.framework.tool.core.exception.BizException;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.github.yitter.idgen.YitIdHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;

/**
 * 文件上传记录
 *
 * @author shenguangyang
 */
@Slf4j
@Service
public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRecord>
    implements IService<FileRecord>, FileRecordService {

    @Resource
    private FileConfigManager fileConfigManager;
    @Resource
    private DictItemApi dictItemApi;
    @Resource
    private FileRecordAssembly fileRecordAssembly;
    @Resource
    private FileProperties fileProperties;

    @Resource
    private FileManager fileManager;
    @Resource
    private StorageChannelManager channelManager;

    @Override
    public void uploadCheck(MultipartFile file) {
        String fileName = FileUtils.getFileName(file);
        String fileExtension = FilenameUtils.getExtension(fileName).toLowerCase();
        if (fileExtension.length() == 0 || fileExtension.equals("dll") || fileExtension.equals("exe") || fileExtension.equals("msi")) {
            throw new BizException(fileName + "文件不允许上传");
        }
    }

    /**
     * 分页查询 文件上传记录
     *
     * @return 返回分页数据
     */
    @Override
    @FieldBindHandle
    public IPage<FileRecordResp> pageFileRecord(FileRecordPageReq req) {
        LocalDateTime beginTime = req.getBeginLocalDateTime();
        LocalDateTime endTime = req.getEndLocalDateTime();
        LambdaQueryWrapper<FileRecord> queryWrapper = FileRecord.lqw()
            .eq(StringUtils.isNotEmpty(req.getBizType()), FileRecord::getBizType, req.getBizType())
            .like(StringUtils.isNotEmpty(req.getOriginalName()), FileRecord::getOriginalName, req.getOriginalName())
            .like(StringUtils.isNotEmpty(req.getNewName()), FileRecord::getNewName, req.getNewName())
            .eq(StringUtils.isNotEmpty(req.getFileType()), FileRecord::getFileType, req.getFileType())
            .eq(Objects.nonNull(req.getCreatedAt()), FileRecord::getCreatedAt, req.getCreatedAt())
            .ge(Objects.nonNull(beginTime), FileRecord::getCreatedAt, beginTime)
            .le(Objects.nonNull(endTime), FileRecord::getCreatedAt, endTime)
            .orderByDesc(FileRecord::getCreatedAt);
        IPage<FileRecord> pageData = this.page(PageUtil.to(req), queryWrapper);

        List<String> bizTypes = pageData.getRecords().stream().map(FileRecord::getBizType).distinct().collect(Collectors.toList());

        List<FileConfig> fileConfigList = fileConfigManager.listFileConfigByBizTypes(bizTypes);
        Map<String, FileConfig> fileConfigMap = fileConfigList.stream().collect(Collectors.toMap(FileConfig::getBizType, Function.identity()));
        return  PageUtil.to(pageData, (e) -> {
            FileRecordResp rs = fileRecordAssembly.toFileRecordResp(e);
            FileConfig fileConfig = fileConfigMap.get(rs.getBizType());
            if (Objects.nonNull(fileConfig)) {
                String virtualFileUrl = fileManager.getVirtualFileUrl(e.getFilePath());
                rs.setFileUrl(virtualFileUrl);
            }
            return rs;
        });
    }

    /**
     * 根据主键查询 文件上传记录
     *
     * @param id 主键
     * @return 返回记录，没有返回null
     */
    @Override
    @FieldBindHandle
    public FileRecordResp getRecordById(Long id) {
        FileRecord entity = this.getById(id);
        FileRecordResp rs = fileRecordAssembly.toFileRecordResp(fileProperties, entity);

        FileConfig fileConfig = fileConfigManager.getFileConfigByBizType(entity.getBizType());
        if (Objects.nonNull(fileConfig)) {
            String virtualFileUrl = fileManager.getVirtualFileUrl(entity.getFilePath());
            rs.setFileUrl(virtualFileUrl);
        }
        return rs;
    }

    @Override
    public FileRecordResp getRecordByPath(String path, boolean isIntranet) {
        FilePathBO filePathBO = FilePathBO.parsePath(path);
        FileRecord entity = this.getById(filePathBO.getFileRecordId());
        Assert.notNull(entity, new BizException("文件记录不存在"));
        FileRecordResp rs = fileRecordAssembly.toFileRecordResp(fileProperties, entity);
        FileConfig fileConfig = fileConfigManager.getFileConfigByBizType(entity.getBizType());
        if (Objects.nonNull(fileConfig)) {
            if (isIntranet) {
                IStorageChannel storageChannel = channelManager.getStorageChannel();
                String intranetUrl = storageChannel.getIntranetUrl(rs.getBucketName(), rs.getFilePath());
                rs.setFileUrl(intranetUrl);
            } else {
                String virtualFileUrl = fileManager.getVirtualFileUrl(entity.getFilePath());
                rs.setFileUrl(virtualFileUrl);
            }
        }
        return rs;
    }

    /**
     * 保存 文件上传记录
     *
     * @param req 新增的记录
     * @return 返回影响行数
     */
    @Override
    public boolean saveFileRecord(FileRecord req) {
        req.setId(null);
    	return this.save(req);
    }

    /**
     * 通过id删除 文件上传记录
     *
     * @param id 删除的主键
     * @return 返回影响行数
     */
    @Override
    public boolean deleteRecordById(Long id) throws Exception {
        FileRecord fileRecord = this.getById(id);
        if (Objects.isNull(fileRecord)) {
            throw new BizException(GlobalErrorEnum.OPERATION_FAIL_SELETE);
        }
        IStorageChannel storageChannel = channelManager.getStorageChannel();
        storageChannel.deleteObject(fileRecord.getFilePath());
    	return this.removeById(id);
    }
    
    /**
     * 通过ids删除 文件上传记录
     *
     * @param ids 删除的主键
     * @return 返回影响行数
     */
    @Override
    @Transactional
    public boolean deleteRecordByIds(List<Long> ids) throws Exception {
        if (CollectionUtils.isEmpty(ids)) {
            return false;
        }
        List<FileRecord> fileRecord = this.listByIds(ids);
        if (CollectionUtils.isEmpty(fileRecord)) {
            throw new BizException(GlobalErrorEnum.OPERATION_FAIL_SELETE);
        }

        for (FileRecord record : fileRecord) {
            IStorageChannel storageChannel = channelManager.getStorageChannel();
            storageChannel.deleteObject(record.getFilePath());
        }
    	return this.removeByIds(ids);
    }

    @Override
    public FileRecordSaveResp genFileRecord(UploadFileReq req) {
        String fileName = FileUtils.getFileName(req.getFile());
        String fileExtension = FilenameUtils.getExtension(fileName).toLowerCase();
        MultipartFile file = req.getFile();
        String bizType = req.getBizType();
        LoginUser loginUser = SecurityUtils.getLoginUser();

        log.info("上传文件 ===> name [{}] ext [{}]", fileName, fileExtension);
        String path = "";
        // step 判断是不是需要覆盖原文件, 为了安全起见(避免覆盖其他用户图片), 需要对原文件文件url进行验签
        if (StringUtils.isNotEmpty(req.getOriginUrl())) {
            boolean ret = FileUtils.checkUrlSign(req.getUrlSign(), req.getOriginUrl());
            if (!ret) {
                throw new BizException("文件地址签名校验失败");
            }
            path = FileUtils.getObjectNameFormPath(req.getOriginUrl());
        }

        // 检查文件
        FileConfig fileConfig = fileConfigManager.getFileConfigByBizType(bizType);
        if (Objects.isNull(fileConfig)) {
            throw new BizException("{} 业务类型不存在", bizType);
        }

        // 检查是否被禁用
        if (BoolEnum.FALSE.getCode().equals(fileConfig.getStatus())) {
            throw new BizException("{} 业务类型被禁用", fileConfig.getBizType());
        }

        // 检查文件大小
        Long fileSizeLimit = fileConfig.getFileSizeLimit();
        if (Objects.nonNull(fileSizeLimit) && fileSizeLimit.compareTo(file.getSize() / 1024) < 0) {
            throw new BizException("文件大小超出限制, 最大为 {} kb", fileSizeLimit);
        }

        // 检查文件类型
        List<String> fileTypeLimit = StringUtils.toList(fileConfig.getFileTypeLimit(), ",", String.class);
        if (CollectionUtils.isNotEmpty(fileTypeLimit) && !StringUtils.matches(file.getContentType(), fileTypeLimit)) {

            throw new BizException("不被允许的content-type类型 {}", file.getContentType());
        }

        IStorageChannel storageChannel = channelManager.getStorageChannel();
        Long fileRecordId = YitIdHelper.nextId();
        Long userId = loginUser.getUserId();
        FilePathBO filePathBO = new FilePathBO(fileConfig, fileExtension, String.valueOf(fileRecordId), userId);
        path = StringUtils.firstNonEmpty(path, filePathBO.getPath());
        FileRecord fileRecord = FileRecord.builder().id(fileRecordId).bucketName(fileProperties.getBucketName())
                .filePath(path).fileSize(file.getSize() / 1024).fileType(file.getContentType()).bizType(bizType)
                .newName(filePathBO.getNewName()).originalName(file.getOriginalFilename())
                .storageType(storageChannel.storageType().getCode())
                .build();
        this.save(fileRecord);

        FileRecordSaveResp resp = new FileRecordSaveResp();
        resp.setFileRecord(fileRecord);
        resp.setFileConfig(fileConfig);
        return resp;
    }

    @Override
    public void download(HttpServletResponse response, Long recordId) throws IOException {
        FileRecord fileRecord = this.getById(recordId);
        if (Objects.isNull(fileRecord)) {
            return;
        }

        IStorageChannel storageChannel = channelManager.getStorageChannel();
        String contentType = fileRecord.getFileType();
        InputStream is = storageChannel.getFile(fileRecord.getFilePath());
        FileDownloadUtils.download(is, is.available(),  contentType, fileRecord.getOriginalName(), true, response);
    }

    @Override
    public void deleteRecordByPath(String path) {
        try {
            FilePathBO filePathBO = FilePathBO.parsePath(path);
            FileRecord dbRecord = this.getById(filePathBO.getFileRecordId());
            if (Objects.isNull(dbRecord)) {
                return;
            }

            IStorageChannel storageChannel = channelManager.getStorageChannel();
            storageChannel.deleteObject(dbRecord.getFilePath());
            this.removeById(dbRecord.getId());
        } catch (Exception e) {
            log.error("delete file path, error: ", e);
        }
    }
}
